package main

import (
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net"
	"net/http"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/PuerkitoBio/goquery"
	"github.com/go-ping/ping"
)

type Node struct {
	id       string
	linetype string
	pingRsp  *PingRsp

	localPingRtt time.Duration
}

func (node *Node) String() string {
	if node.pingRsp == nil {
		return fmt.Sprintf("id:%s,linetype:%s, ping is nil", node.id, node.linetype)
	}
	return fmt.Sprintf("id:%s,linetype:%s, pingRsp:%v", node.id, node.linetype, node.pingRsp)
}

type PingRsp struct {
	ip           string
	ipaddress    string
	responsetime int
	ttl          int
	bytes        int
}

func QueryServerNodeIdList(host, regions string) ([]*Node, string) {
	url := "http://ping.chinaz.com/" + host
	contentType := "application/x-www-form-urlencoded; charset=UTF-8"
	data := strings.NewReader(fmt.Sprintf("host=%s&linetype=%s", host, regions))
	rsp, err := http.Post(url, contentType, data)
	if err != nil || rsp.StatusCode != 200 {
		log.Printf("QueryServerNodeIdList http err:%v,%d,%s", err, rsp.StatusCode, rsp.Status)
		return nil, ""
	}
	defer rsp.Body.Close()

	// Load the HTML document
	doc, err := goquery.NewDocumentFromReader(rsp.Body)
	if err != nil {
		log.Fatal(err)
	}

	var nodeList []*Node
	doc.Find(".row.listw.tc.clearfix").Each(func(i int, s *goquery.Selection) {
		id, exist := s.Attr("id")
		if exist {
			linetype, _ := s.Attr("linetype")
			nodeList = append(nodeList, &Node{
				id:       id,
				linetype: linetype,
			})
			//log.Printf("node %d: %v, %v\n",i, id, linetype)
		}
	})
	key := doc.Find("#enkey")
	//id, _ := key.Attr("id")
	value, _ := key.Attr("value")
	//log.Printf("key:%v, %v, %v\n", key, id, value)
	return nodeList, value
}

func ParsePingRsp(s string) (*PingRsp, error) {
	start := "result:{"
	pos1 := strings.Index(s, start)
	if pos1 == -1 {
		return nil, fmt.Errorf("format error: not contains result:%s", s)
	}

	rsp := &PingRsp{}
	vs := strings.Split(s[pos1+len(start):], ",")
	for _, kvStr := range vs {
		kv := strings.Split(kvStr, ":")
		if len(kv) < 2 {
			continue
		}
		k := kv[0]
		v := strings.Trim(kv[1], "'")
		reg := regexp.MustCompile(`[0-9]+`)
		//log.Printf("kv:%s, %s", k, v)
		switch k {
		case "ip":
			rsp.ip = strings.Trim(v, "'")
		case "ipaddress":
			rsp.ipaddress = strings.Trim(v, "'")
		case "responsetime":
			rsp.responsetime, _ = strconv.Atoi(reg.FindString(v))
		case "ttl":
			rsp.ttl, _ = strconv.Atoi(reg.FindString(v))
		case "bytes":
			rsp.bytes, _ = strconv.Atoi(reg.FindString(v))
		}
	}
	if !IsIP(rsp.ip) {
		return nil, fmt.Errorf("parse error:%v, %s", rsp, s)
	}
	return rsp, nil
}

func ServerPing(id, host, enKey string) (*PingRsp, error) {
	url := "http://ping.chinaz.com/iframe.ashx?t=ping"
	contentType := "application/x-www-form-urlencoded; charset=UTF-8"
	data := strings.NewReader(fmt.Sprintf("guid=%s&host=%s&ishost=0&isipv6=0&encode=%s&checktype=0", id, host, enKey))
	rsp, err := http.Post(url, contentType, data)
	if err != nil || rsp.StatusCode != 200 {
		return nil, fmt.Errorf("ping err:%v,%d,%s", err, rsp.StatusCode, rsp.Status)
	}
	defer rsp.Body.Close()

	body, err := ioutil.ReadAll(rsp.Body)
	if err != nil {
		return nil, fmt.Errorf("read http rsp data err:%v", err)
	}
	//log.Printf(string(body))
	return ParsePingRsp(string(body))
}

func IsIP(s string) bool {
	address := net.ParseIP(s)
	if address == nil {
		return false
	} else {
		return true
	}
}

func IsPingOK(host string) (bool, time.Duration) {
	pinger, err := ping.NewPinger(host)
	if err != nil {
		log.Printf("NewPinger err:%v", err)
		return false, 0
	}
	pinger.Count = 3
	pinger.Interval = 500 * time.Millisecond
	pinger.Timeout = 3 * time.Second
	pinger.SetPrivileged(true)
	err = pinger.Run() // Blocks until finished.
	if err != nil {
		log.Printf("pinger.Run err:%v", err)
		return false, 0
	}
	stats := pinger.Statistics() // get send/receive/duplicate/rtt stats
	log.Printf("ping :%s, %+v", host, stats)
	if stats.PacketLoss > 0 {
		return false, 0
	}
	return true, stats.AvgRtt
}

func QueryAvailableNodes(host, regions string) ([]*Node, error) {
	nodeList, enKey := QueryServerNodeIdList(host, regions)
	if len(nodeList) == 0 || len(enKey) == 0 {
		return nil, errors.New("query node id error")
	}
	log.Printf("node num:%d, %s", len(nodeList), enKey)

	var availableNodes []*Node
	gotNum := 0
	for _, node := range nodeList {
		//log.Printf("ping req:%s, %s, %s\n", node.id, host, enKey)
		var err error
		node.pingRsp, err = ServerPing(node.id, host, enKey)
		if err != nil {
			log.Printf("ServerPing err:%s, %v", node.id, err)
			continue
		}
		log.Printf("ping rsp:%+v\n", node.pingRsp)
		if node.pingRsp.responsetime == 0 {
			continue
		}
		ok, rtt := IsPingOK(node.pingRsp.ip)
		if !ok {
			log.Printf("local ping error:%s\n", node.pingRsp.ip)
			continue
		}
		node.localPingRtt = rtt
		if len(availableNodes) == 0 {
			availableNodes = append(availableNodes, node)
		}
		if rtt < 300*time.Millisecond {
			availableNodes = append(availableNodes, node)
			gotNum++
			if gotNum > 10 {
				break
			}
		}
	}
	if len(availableNodes) == 0 {
		return nil, errors.New("available nodes not found")
	}
	fmt.Printf("available node num:%d,%+v\n", len(availableNodes), availableNodes)
	return availableNodes, nil
}
